package apibasej.basic.thread;

import java.util.ArrayList;
import java.util.logging.Level;

import apibasej.basic.InterfaceExec;
import apibasej.basic.config.ConfigManager;
import apibasej.basic.exception.APIBaseException;

public class SingleThreadTimer extends ConfigManager{

	private SingleThreadTimer(){}
	
	private ArrayList<ElemT> elems = new ArrayList<>();
	
	private long clockMilis = 180;
	public void setClockMilis(long clockMilis) {
		this.clockMilis = clockMilis;
	}
	public long getClockMilis() {
		return clockMilis;
	}
	
	private static SingleThreadTimer inst;
	public static SingleThreadTimer getInst(){
		if(inst==null){
			inst = new SingleThreadTimer();
			Thread t = new Thread(){
				public void run(){
					try{
						inst.go();
					}finally{
						ConfigManager.logDefAll(Level.WARNING,"SingleTimer: end of thread",null);
						inst = null;
					}
				};
			};
			t.setPriority(Thread.MIN_PRIORITY);//para não atrapalhar outras ações principais
			t.start();
		}
		return inst;
	}

	
	private void go(){
		while(inst!=null){
			try{
				Thread.sleep(clockMilis);
				
				//não usar for para iterar, para não ocorrer CurrentModificationException
				//fazer loop do final para o início, para se remover não afetar a ordem os próximos
				//int size = elems.size();
				
				//usa elems.toArray para não ter perigo de ocorrer CurrentModificationException, 
				//ou ArrayIndexOfBoundException se usar pelo get(i) pode dar erro se remover algum item entre o elems.size() e elems.get(i) ...
				ElemT[] es = elems.toArray(new ElemT[0]); //não
				
				for(ElemT e : es){ //int i=size-1;i>=0;i--//não varrer de traz para frente! pois é preciso mantes a ordem correta!
					//final ElemT e = elems.get(i);
					if(e==null){//ver porque toArray() pode retornar item null
						continue;
					}
					
					// não é preciso, pois pode ter sido adicionado depois de já ter esperado um tempo, 
					//quase sempre irá excutar um pouco antes, 
					//será mais somente se adicionar durante a execução dos itens do loop
					//(pode-se mudar para verificar pelo time da última execução com o time atual, se ultrapassou)
					//(desta forma atual pode demorar mais, caso a lista tenha itens que demorem para executar ...)
					//(mas por enquanto é melhor deixar assim por causa do melhor desempenho)
					e.elapsedTime += clockMilis; 
					
					//se está no tempo de executar
					if(e.elapsedTime>=e.timeWait){
						if(e.execInAnotherThread){
							SingleThreadPoolDefault.getInst().getItemPool().execute(e.exec);
						}else{
							e.exec.exec();
						}
						e.elapsedTime = 0;//zera a contagem ao executar, para caso seja persistent ...
						if(!e.persistent){
							elems.remove(e);
						}
					}
				}
				
			}catch(Throwable t){
				logDef(Level.SEVERE,"SingleTimer: error clock",t);
			}
		}
	}
	
	
	
	// espera a execução de todos elementos não persistentes
	public void waitNonPersistents(){
		boolean existsNonPersistents = true;
		while(existsNonPersistents){
			existsNonPersistents = false;
			//usa elems.toArray para não ter perigo de ocorrer CurrentModificationException, 
			//ou ArrayIndexOfBoundException se usar pelo get(i) pode dar erro se remover algum item entre o elems.size() e elems.get(i) ...
			ElemT[] es = elems.toArray(new ElemT[0]); 
			for(ElemT e : es){
				//??? ver porque o toArray() pode retornar item null !!!!!
				if(e!=null && !e.persistent){
					existsNonPersistents = true;
					break;
				}
			}
			if(existsNonPersistents){
				try{
					Thread.sleep(clockMilis);
				}catch(Throwable t){
					logDef(Level.SEVERE, "Error sleep waitNonPersistents()", t);
				}
			}
		}
	}
	
	
	public void addExecClock0(InterfaceExec exec){//fast
		addExec(exec, 0, false, false);//executa na próxima iteração
	}
	public void addExecClock1(InterfaceExec exec){//normal
		addExec(exec, clockMilis+1, false, false);//executa somente na segunda volta, geralmente irá demorar um pouco mais que o clock
	}
	public void addExecClock2(InterfaceExec exec){//prioridade menor
		addExec(exec, clockMilis*2+1, false, false);//executa somente na terceira volta, geralmente irá demorar um pouco mais que o clock*2
	}
	public void addExec(InterfaceExec exec, long timeWaitMillis){
		addExec(exec, timeWaitMillis, false, false);
	}
	
	//para adiantar a execução de um exec para a próxima iteração
	//o exec deve estar adicionado antes 
	public void advanceExec(InterfaceExec exec){
		//verifica se o mesmo exec já está na lista, se já está ignora ... (ter flag para não fazer esta verificação?)
		//usa elems.toArray para não ter perigo de ocorrer CurrentModificationException, 
		//ou ArrayIndexOfBoundException se usar pelo get(i) pode dar erro se remover algum item entre o elems.size() e elems.get(i) ...
		ElemT[] es = elems.toArray(new ElemT[0]); 
		for(ElemT e : es){
			if(e!=null && e.exec==exec){
				e.elapsedTime = e.timeWait+1;
			}
		}
	}
	
	public void addExec(InterfaceExec exec, long timeWaitMillis, boolean persistent, boolean execInAnotherThread){ //geralmente se for persistent deve executar em outra thread para não atrapalhar as outras ...
		
		if(exec==null){
			throw new NullPointerException("InterfaceExec cannot be null!");
		}
		
		/* ElemT[] es = elems.toArray(new ElemT[0]);  pode ter item null ????????? como ???
		Caused by: java.lang.NullPointerException
		at apibasej.infra.SingleTimer.addExec(SingleTimer.java:130)
		at apibasej.infra.SingleTimer.addExecClock1(SingleTimer.java:111)
		at juliolessa.trmanager.client.implswing.impldef.util.ExecUpdateSwing.<init>(ExecUpdateSwing.java:13)
		*/
		//verifica se o mesmo exec já está na lista, se já está ignora ... (ter flag para não fazer esta verificação?)
		//usa elems.toArray para não ter perigo de ocorrer CurrentModificationException, 
		//ou ArrayIndexOfBoundException se usar pelo get(i) pode dar erro se remover algum item entre o elems.size() e elems.get(i) ...
		ElemT[] es = elems.toArray(new ElemT[0]); 
		
		//esta verificação não é precisa, pois pode mudar o tamanho entre a geração do array e a rerificação !!!
		//if(es.length!=elems.size()){//para detectar se o toArray pode ter elementos qantigos que já foram removidos ...
		//	throw new RuntimeException(" es.length!=elems.size() !!! ");
		//}
		
		ElemT e = null;
		
		//System.out.println("-----------------------------");
		for(ElemT e1 : es){
			//ver porque o toArray() pode retornar item null !!!!!
			//if(e==null){//para detectar se o toArray pode ter elementos qantigos que já foram removidos ... ou outra situação que ElemT
			//	throw new RuntimeException(" ElemT == null !!! ");
			//}
			//não adiciona um exec se ele já existe na lista
			//System.out.println(">>>>("+(e1.exec==exec)+")>> e1.exec="+e1.exec+"("+e1.persistent+"), exec="+exec+" | "+es.length);
			if(e1!=null && e1.exec==exec){
				e = e1;
				break;
			}
		}
		boolean isNewElem = false;
		if(e==null){
			e = new ElemT();
			isNewElem = true;
		}
		e.exec = exec;
		e.timeWait = timeWaitMillis;
		e.persistent = persistent;
		e.execInAnotherThread = execInAnotherThread;
		
		if(isNewElem){//só deve adicionar se for novo, pois se não for ja deve estar na lista
			elems.add(e);
			try{
				if(elems.size()>200){//somente para detectar se tiver algum bug ... ?????? TODO depois diminuir esta verificação ??? ver como otimizar ...
					StringBuilder infoExecs = new StringBuilder();
					for(ElemT et : elems){
						infoExecs.append("\n"+et.exec+"_"+(et.exec!=null?et.exec.getInfo():"NULL")+";");
					}
					APIBaseException ex = new APIBaseException(getClass().getSimpleName()+": Mais de 200 itens adicionados para execução ("+elems.size()+")! "+infoExecs.toString());
					logDef(Level.WARNING, ex.getMessage(), ex);
				}
			}catch(Throwable t1){
				//pode acontecer null pointer no et.exec.toString(), se tiver muitas chamadas concorrentes a este método
				logDef(Level.WARNING, "Erro ao ler informações dos elementos de execução na "+getClass().getSimpleName(), t1);
			}
		}
	}
	
	private class ElemT{
		InterfaceExec exec = null;
		long timeWait = 0;//em milisegundos
		boolean persistent = false;//se for true fica sempre vivo na lista
		boolean execInAnotherThread = false;//se quiser que a execução seja feita em thread separada, para não atrapalhar a execução da lista
		//Long timeToExec = null;//caso queira definir o horário para executar ... ver opção hora, dia, ... melhor não ... pois a verificação pode demorar, se quiser executar em um horário colocar uma persistent com um tempo maior e verificar a data dentro da execução
		long elapsedTime = 0;
		
		@Override
		public boolean equals(Object obj) {
			return this==obj;
		}
	}
	
}



